import CodoError from "../../../lib/CodoError";
import * as crypto from "crypto";
import { hikvision } from "./type";
import { hikvisionAccessTokenKey } from "../../util";
import { HikvisionBaseService } from "./base";
import { URL } from "url";

/**
 * hikvision isc auth class
 */
export class AuthService extends HikvisionBaseService {
	/** 获取accessToken
	 * - 优先尝试从本地cache获取，获取不到会从isc获取，如果过期会自动重新从isc获取并缓存起来
	 */
	async getAccessToken() {
		type DataObj = { time: number } & hikvision.isc.IGetAccessTokenResponse;

		const dataKey = hikvisionAccessTokenKey(this.config.appKey);
		const cache = await this.dataManager.get<DataObj>(dataKey);
		if (cache && cache.time + cache.expires_in * 1000 > Date.now()) {
			return cache;
		}
		const r = await this.getAccessTokenFromISC();
		const data: DataObj = {
			time: Date.now(),
			...r,
		};
		await this.dataManager.save(dataKey, data);
		return data;
	}

	/** 请求isc平台获取token */
	async getAccessTokenFromISC() {
		const url = `/api/v1/oauth/token`;
		const headers: Headers = {
			"Content-Type": "application/json",
			Accept: "application/json",
			"X-Ca-Nonce": crypto.randomBytes(16).toString("hex"),
			"X-Ca-Timestamp": Date.now().toString(),
		};

		const fullUrl = `${this.config.baseUrl}${url}`;
		const theUrl = new URL(fullUrl);
		// theUrl.pathname
		const signed = this.sign("POST", headers, theUrl.pathname, {}, {});
		const res = await this.curl<
			hikvision.isc.IResponse<hikvision.isc.IGetAccessTokenResponse>
		>(fullUrl, {
			method: "POST",
			headers: signed.headers,
			data: {},
			dataType: "json",
			timeout: 10000,
			rejectUnauthorized: false,
		});

		this.handleResponseException(res, `getAccessToken Error`);
		this.setAccessToken(res.data.data.access_token);
		res.data.data.expires_in = Number(res.data.data.expires_in);
		return res.data.data;
	}

	/**
	 * 按文档进行headers签名
	 */
	private sign(
		method: string,
		headers: Headers,
		path: string,
		query: Record<string, any>,
		bodyForm: Record<string, any>
	) {
		const regex = /^[A-Z]+$/; // 大写字母
		if (!regex.test(method)) {
			throw new CodoError(`sign：the http method must be uppercase`);
		}
		const accept = headers["Accept"];
		const contentMD5 = headers["Content-MD5"];
		const contentType = headers["Content-Type"];
		const date = headers["Date"];
		/** 拼接好的待签名字符串 */
		let str = `${method}\n`;
		// step 1 固定字段拼接
		str += accept !== undefined ? `${accept}\n` : "";
		str += contentMD5 !== undefined ? `${contentMD5}\n` : ""; // 暂不考虑计算body的md5
		str += contentType !== undefined ? `${contentType}\n` : "";
		str += date !== undefined ? `${date}\n` : "";

		// step 2 自定义headers拼接

		// 只取这一个字段加入签名计算
		const myKey = headers["X-Ca-Timestamp"];
		const xcaTimestamp = "X-Ca-Timestamp".toLowerCase();
		str += `${xcaTimestamp}:${myKey}\n`;

		headers["X-Ca-Signature-Headers"] = xcaTimestamp; // 参与签名的header放入此字段

		headers["X-Ca-Key"] = this.config.appKey;

		// step 3 拼接url query和bodyForm

		const queryBodyForm = { ...query, ...bodyForm };
		const sortedQueryBodyStr = Object.keys(queryBodyForm)
			.sort()
			.map((i) => `${i}=${queryBodyForm[i]}`)
			.join("&");

		let pathAllStr = path;
		if (sortedQueryBodyStr) {
			pathAllStr += "?" + sortedQueryBodyStr;
		}
		// 内置将query排序后放入url中
		const queryStr = Object.keys(query)
			.sort()
			.map((i) => `${i}=${query[i]}`)
			.join("&");

		// 拼接完成的待签名原始字符串
		str += pathAllStr;

		const signature = crypto
			.createHmac("sha256", this.config.appSecret)
			.update(str)
			.digest("base64");

		headers["X-Ca-Signature"] = signature;

		return {
			str,
			queryStr,
			headers,
		};
	}
}

interface Headers {
	Accept?: string;
	"Content-MD5"?: string;
	"Content-Type"?: string;
	Date?: string;
	"X-Ca-Key"?: string;
	"X-Ca-Signature"?: string;
	"X-Ca-Signature-Headers"?: string;
	"X-Ca-Timestamp": string;
	"X-Ca-Nonce": string;

	[key: string]: string | undefined;
}
